SQLiteは軽量で高速ですが、設計次第で「爆速」にも「もっさり」にもなるデータベースです。 特に業務アプリでは、ユーザーが毎日触る画面ほど体感速度が重要になります。
この記事では、SQLiteを使ったアプリで 体感速度を劇的に上げるキャッシュ戦略を、実務目線で整理します。
・SQLiteでキャッシュが必要になる理由
・メモリキャッシュ(Dictionary / MemoryCache)
・結果キャッシュ(クエリ結果の再利用)
・読み取り専用キャッシュDB(サブDB)
・APIレスポンスキャッシュ × SQLite
・WPF/MVVMとの組み合わせパターン
・業務アプリ向けベストプラクティス
1. なぜSQLiteでもキャッシュが必要になるのか?
SQLiteは速いとはいえ、次のようなケースでは体感的に遅く感じることがあります。
- 毎回同じマスタをSELECTしている
- 重いJOINを何度も実行している
- DataGridに大量データを頻繁に表示する
- API経由で取得したデータを毎回DBに書き込んでいる
こうした「何度も同じデータを読む」パターンに対して、 キャッシュ戦略を入れると体感速度が一気に変わります。
2. キャッシュ戦略の全体像
SQLite × 業務アプリで使えるキャッシュは大きく4種類あります。
- メモリキャッシュ(アプリ内メモリ)
- 結果キャッシュ(クエリ結果の再利用)
- 読み取り専用キャッシュDB(サブSQLite)
- APIレスポンスキャッシュ(サーバー側 or クライアント側)
これらを組み合わせることで、DBアクセス回数を減らし、体感速度を最大化できます。
3. メモリキャッシュ(アプリ内メモリ)
最もシンプルで効果が高いのが、メモリ上にマスタや設定を保持する方法です。
■ 典型的な対象
- コードマスタ(区分・種別・ステータス)
- 都道府県・市区町村などの地理マスタ
- アプリ設定・ユーザー設定
■ シンプルなDictionaryキャッシュ
public class MasterCache
{
private readonly IConnectionFactory _factory;
private Dictionary<int, string> _statusCache = new();
public MasterCache(IConnectionFactory factory)
{
_factory = factory;
}
public async Task<string?> GetStatusNameAsync(int statusId)
{
if (_statusCache.TryGetValue(statusId, out var name))
return name;
using var con = _factory.CreateConnection();
var sql = "SELECT Name FROM Statuses WHERE Id=@id";
name = await con.ExecuteScalarAsync<string?>(sql, new { id = statusId });
if (name != null)
_statusCache[statusId] = name;
return name;
}
}
一度取得したら、以降はメモリから即返却されます。
4. 結果キャッシュ(クエリ結果の再利用)
「同じ検索条件で何度も検索される」画面では、 クエリ結果そのものをキャッシュすると効果的です。
■ キーを工夫する
- 検索条件を文字列化してキーにする
- ページ番号・ソート条件も含める
■ シンプルな結果キャッシュ例
public class QueryCache<T>
{
private readonly Dictionary<string, List<T>> _cache = new();
public bool TryGet(string key, out List<T> value)
=> _cache.TryGetValue(key, out value!);
public void Set(string key, List<T> value)
=> _cache[key] = value;
}
DataGridの再表示やタブ切り替え時に、DBアクセスなしで即座に表示できます。
5. 読み取り専用キャッシュDB(サブSQLite)
「参照専用の重いデータ」を別のSQLiteファイルに切り出し、 読み取り専用キャッシュDBとして扱う方法もあります。
■ 典型的な対象
- 過去の履歴データ(更新しない)
- ログ・トレース・監査情報
- 集計済みデータ(レポート用)
■ メリット
- 本番DB(トランザクションDB)への負荷を減らせる
- バックアップ・ローテーションがしやすい
- 読み取り専用なのでロックがほぼ発生しない
アプリ起動時にキャッシュDBを更新(再生成)する構成も有効です。
6. APIレスポンスキャッシュ × SQLite
サーバー側にAPIがある場合、 APIレスポンスをSQLiteにキャッシュすることで、 ネットワーク遅延を隠すことができます。
■ クライアント側キャッシュの流れ
- API呼び出し前にローカルSQLiteをチェック
- キャッシュがあれば即返却
- バックグラウンドでAPIを叩いて更新
■ キャッシュテーブル例
CREATE TABLE ApiCache (
Key TEXT PRIMARY KEY,
Json TEXT NOT NULL,
UpdatedAt TEXT NOT NULL
);
Key に URL + クエリ文字列を入れておくと汎用的に使えます。
7. WPF/MVVMとの組み合わせパターン
WPF/MVVMでは、キャッシュをサービス層 or Repository層に閉じ込めると綺麗にまとまります。
■ ViewModel側は「キャッシュの存在を知らない」構造
public class UserListViewModel
{
private readonly IUserService _service;
public ObservableCollection<UserDto> Users { get; } = new();
public async Task LoadAsync()
{
var list = await _service.GetUsersAsync(); // 中でキャッシュ利用
Users.Clear();
foreach (var u in list)
Users.Add(u);
}
}
キャッシュの有無・更新タイミングはすべて サービス層で制御するのがポイントです。
8. キャッシュの「無効化戦略」も必ず決めておく
キャッシュは速くなる代わりに「古くなる」というリスクがあります。 そのため、必ず「いつ捨てるか」を決めておきます。
■ よくある無効化ルール
- アプリ起動ごとにクリア
- 一定時間(5分・1時間)で期限切れ
- ユーザー操作(更新ボタン)で明示的にリロード
- UpdatedAt / Version を見て差分更新
9. 業務アプリ向けベストプラクティス
- マスタ・設定はメモリキャッシュで保持
- 検索結果は結果キャッシュで再利用
- 履歴・ログは読み取り専用キャッシュDBに分離
- APIレスポンスはSQLiteにキャッシュしてオフライン対応も視野に
- キャッシュの無効化ルールを必ず決める
- キャッシュの実装はサービス層に閉じ込める
まとめ:SQLite × キャッシュ戦略は“体感速度”を決める最後の一手
- SQLiteは速いが、キャッシュを組み合わせると「別物レベル」に速くなる
- マスタ・検索結果・履歴・APIレスポンスなど、対象ごとに戦略を変える
- キャッシュは「どこに置くか」「いつ捨てるか」をセットで設計する
「クエリはそこそこ速いのに、アプリが重く感じる」 そんなときこそ、キャッシュ戦略の出番です。 この記事をベースに、あなたのアプリに最適な高速化パターンを設計してみてください。